﻿using System;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace CodeAnalysisRuleCollection.Common.CSharp
{
    /// <example>throw new ArgumentException(paramName: "notExists", message: null);</example>
    [DiagnosticAnalyzer]
    [ExportDiagnosticAnalyzer(DiagnosticId, LanguageNames.CSharp)]
    internal class InvalidParameterNameInArgumentExceptionDiagnosticAnalyzer : ISyntaxNodeAnalyzer<SyntaxKind>
    {
        #region Description
        public const string DiagnosticId = "InvalidParameterNameInArgumentException";

        public static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId,
            "Invalid parameter name",
            "Parameter with name '{0}' could not be found in the enclosing method",
            "Usage",
            DiagnosticSeverity.Error
        );

        public ImmutableArray<SyntaxKind> SyntaxKindsOfInterest { get { return ImmutableArray.Create(SyntaxKind.ObjectCreationExpression); } }

        public ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }
        #endregion

        public void AnalyzeNode(SyntaxNode node, SemanticModel semanticModel, Action<Diagnostic> addDiagnostic, CancellationToken cancellationToken)
        {
            if ((var expression = node as ObjectCreationExpressionSyntax) != null)
            {
                // find type
                ITypeSymbol type = semanticModel.GetTypeInfo(expression, cancellationToken).Type;
                ITypeSymbol argumentException = semanticModel.Compilation.GetTypeByMetadataName(typeof(ArgumentException).FullName);

                if (!InheritsFrom(type, argumentException))
                    return;

                // find argument at location of paramName parameter
                ArgumentSyntax argumentSyntax = expression.ArgumentList.Arguments.SingleOrDefault(
                    a => a.NameColon != null && a.NameColon.Name.Identifier.ValueText == "paramName"
                );

                if (argumentSyntax == null)
                {
                    IMethodSymbol constructor = semanticModel.GetSymbolInfo(expression, cancellationToken).Symbol as IMethodSymbol;
                    if (constructor == null || constructor.MethodKind != MethodKind.Constructor)
                        return;

                    IMethodSymbol matching = type.GetMembers(".ctor").SingleOrDefault(c => c.Equals(constructor)) as IMethodSymbol;
                    if (matching == null)
                        return;

                    for (int i = 0; i < matching.Parameters.Length; i++)
                    {
                        if (matching.Parameters[i].Name == "paramName")
                        {
                            argumentSyntax = expression.ArgumentList.Arguments[i];
                            break;
                        }
                    }
                }

                if (argumentSyntax == null)
                    return;

                // get argument name
                Optional<object> constant = semanticModel.GetConstantValue(argumentSyntax.Expression, cancellationToken);

                if (!constant.HasValue)
                    return;

                string parameterName = constant.Value as string;

                if (parameterName == null)
                {
                    addDiagnostic(Diagnostic.Create(Rule, argumentSyntax.Expression.GetLocation()));
                    return;
                }

                // find enclosing method
                SyntaxNode enclosingDeclaration = argumentSyntax.FirstAncestorOrSelf<SyntaxNode>(n =>
                    n is MethodDeclarationSyntax ||
                    n is ConstructorDeclarationSyntax ||
                    n is AccessorDeclarationSyntax ||
                    n is ParenthesizedLambdaExpressionSyntax ||
                    n is SimpleLambdaExpressionSyntax
                );

                IMethodSymbol method = semanticModel.GetDeclaredSymbol(enclosingDeclaration, cancellationToken) as IMethodSymbol
                    ?? semanticModel.GetSymbolInfo(enclosingDeclaration, cancellationToken).Symbol as IMethodSymbol
                ;

                if (method != null && !method.Parameters.Any(p => p.Name == parameterName))
                    addDiagnostic(Diagnostic.Create(Rule, argumentSyntax.Expression.GetLocation(), parameterName));
            }
        }

        private static bool InheritsFrom(ITypeSymbol type, ITypeSymbol baseType)
        {
            while (type != null)
            {
                if (type.Equals(baseType))
                    return true;

                type = type.BaseType;
            }

            return false;
        }
    }
}